﻿using System;
using System.ComponentModel;
using System.Reflection;
using System.Windows;
using System.Linq;
using UnitDriven.Commanding;
#if NETFX_CORE
using Windows.UI.Xaml;
#endif

namespace UnitDriven
{
	public class MethodTester : ObservableObject, IRunTests
	{
		#region Member fields and properties

		private TypeTester _parent;
		private TestResult _status;
		private MethodInfo _method;
		private string _message;
		private bool _isRunning;

		public Visibility IsVisible
		{
			get
			{
				return string.IsNullOrEmpty(_parent.Context.Filter) || this.Name.ToLowerInvariant().Contains(_parent.Context.Filter.ToLowerInvariant())
					? Visibility.Visible
					: Visibility.Collapsed;
			}
		}

		public bool IsRunning
		{
			get { return _isRunning; }
			set
			{
				_isRunning = value;
				OnPropertyChanged("IsRunning");
				OnPropertyChanged("IsNotRunning");
			}
		}

		public bool IsNotRunning
		{
			get { return !IsRunning; }
			set { IsRunning = !value; }
		}

		public bool HasError
		{
			get { return _status == TestResult.Fail; }
		}

		public TestResult Status
		{
			get { return _status; }
			set
			{
				_status = value;
				OnPropertyChanged("Status");
				OnPropertyChanged("HasError");
			}
		}

		public string Message
		{
			get { return _message; }
			set
			{
				_message = value;
				OnPropertyChanged("Message");
			}
		}

		public string Name { get { return _method.Name; } }

		public Type TestType { get { return _parent.TestType; } }

		public MethodInfo Method
		{
			get { return _method; }
		}

		#endregion

		#region Constructors

		public MethodTester(TypeTester parent, MethodInfo method)
		{
			if (parent == null)
				throw new ArgumentNullException("parent");
			if (method == null)
				throw new ArgumentNullException("method");

			if (!method.IsDefined(typeof(TestMethodAttribute), true) && !method.IsDefined(typeof(TestCaseAttribute), true))
				throw new ArgumentException("Method must have TestMethod or TestCase attribute applied");
			if (!method.IsPublic)
				throw new ArgumentException("Method must be public");

			_parent = parent;
			_parent.Context.PropertyChanged += Context_PropertyChanged;
			_method = method;
			_status = TestResult.Evaluating;
		}

		void Context_PropertyChanged(object sender, PropertyChangedEventArgs e)
		{
			if (e.PropertyName == "Filter")
			{
				OnPropertyChanged("IsVisible");
			}
		}

		#endregion

		public void Initialize()
		{
			IsRunning = true;
			Message = null;
			Status = TestResult.Evaluating;
		}

		public void RunTests()
		{
			if (this.Method.IsDefined(typeof(TestMethodAttribute), true))
			{
				EnqueueTestCommand();
			}
			else if (this.Method.IsDefined(typeof(TestCaseAttribute), true))
			{
				foreach (var testCaseAtt in this.Method.GetCustomAttributes(typeof(TestCaseAttribute), true).Cast<TestCaseAttribute>())
				{
					EnqueueTestCommand(
						testCaseAtt.Arguments,
						testCaseAtt.Result,
						testCaseAtt.ExpectedException,
						testCaseAtt.ExpectedExceptionName,
						testCaseAtt.ExpectedMessage,
						testCaseAtt.MatchType);
				}
			}
			else
			{
				throw new NotSupportedException();
			}
		}

		private void EnqueueTestCommand(
			object[] arguments = null,
			object expectedResult = null,
			Type expectedExceptionType = null,
			string expectedExceptionName = null,
			string expectedExceptionMessage = null,
			MessageMatch match = MessageMatch.Exact)
		{
			var command = new TestCommand(
				this.Initialize,
				this.TestType,
				this.Method,
				arguments,
				expectedResult,
				expectedExceptionType != null ? expectedExceptionType.ToString() : expectedExceptionName,
				expectedExceptionMessage,
				match);

			command.Complete += TestCompleted;

			this.numQueuedTests++;
			CommandQueue.Enqueue(command);
		}

		private int numQueuedTests = 0;
		private void TestCompleted(object sender, CommandCompleteEventArgs e)
		{
			var command = (TestCommand)sender;
			var args = (TestCompleteEventArgs)e;
			command.Complete -= TestCompleted;

			numQueuedTests--;
			if (numQueuedTests < 0)
				throw new InvalidOperationException();

			Message += (Environment.NewLine + args.Message);
			Status = Status == TestResult.Fail ? Status : args.Result;
			IsRunning = numQueuedTests > 0;
		}

		public int Succeeded
		{
			get { throw new NotImplementedException(); }
		}
	}
}
